"""
GTSAM Copyright 2010-2020, Georgia Tech Research Corporation,
Atlanta, Georgia 30332-0415
All Rights Reserved

See LICENSE for the license information

Parser classes and rules for parsing C++ classes.

Author: Duy Nguyen Ta, Fan Jiang, Matthew Sklar, Varun Agrawal, and Frank Dellaert
"""

from typing import Any, Iterable, List, Union

from pyparsing import ZeroOrMore  # type: ignore
from pyparsing import Literal, Optional, Word, alphas

from .enum import Enum
from .function import ArgumentList, ReturnType
from .template import Template
from .tokens import (CLASS, COLON, CONST, DUNDER, IDENT, LBRACE, LPAREN,
                     OPERATOR, RBRACE, RPAREN, SEMI_COLON, STATIC, VIRTUAL)
from .type import TemplatedType, Typename
from .utils import collect_namespaces
from .variable import Variable


class Method:
    """
    Rule to parse a method in a class.

    E.g.
    ```
    class Hello {
        void sayHello() const;
    };
    ```
    """
    rule = (
        Optional(Template.rule("template"))  #
        + ReturnType.rule("return_type")  #
        + IDENT("name")  #
        + LPAREN  #
        + ArgumentList.rule("args_list")  #
        + RPAREN  #
        + Optional(CONST("is_const"))  #
        + SEMI_COLON  # BR
    ).setParseAction(lambda t: Method(t.template, t.name, t.return_type, t.
                                      args_list, t.is_const))

    def __init__(self,
                 template: Union[Template, Any],
                 name: str,
                 return_type: ReturnType,
                 args: ArgumentList,
                 is_const: str,
                 parent: Union["Class", Any] = ''):
        self.template = template
        self.name = name
        self.return_type = return_type
        self.args = args
        self.is_const = is_const

        self.parent = parent

    def to_cpp(self) -> str:
        """Generate the C++ code for wrapping."""
        return self.name

    def __repr__(self) -> str:
        return "Method: {} {} {}({}){}".format(
            self.template,
            self.return_type,
            self.name,
            self.args,
            self.is_const,
        )


class StaticMethod:
    """
    Rule to parse all the static methods in a class.

    E.g.
    ```
    class Hello {
        static void changeGreeting();
    };
    ```
    """
    rule = (
        Optional(Template.rule("template"))  #
        + STATIC  #
        + ReturnType.rule("return_type")  #
        + IDENT("name")  #
        + LPAREN  #
        + ArgumentList.rule("args_list")  #
        + RPAREN  #
        + SEMI_COLON  # BR
    ).setParseAction(
        lambda t: StaticMethod(t.name, t.return_type, t.args_list, t.template))

    def __init__(self,
                 name: str,
                 return_type: ReturnType,
                 args: ArgumentList,
                 template: Union[Template, Any] = None,
                 parent: Union["Class", Any] = ''):
        self.name = name
        self.return_type = return_type
        self.args = args
        self.template = template

        self.parent = parent

    def __repr__(self) -> str:
        return "static {} {}{}".format(self.return_type, self.name, self.args)

    def to_cpp(self) -> str:
        """Generate the C++ code for wrapping."""
        return self.name


class Constructor:
    """
    Rule to parse the class constructor.
    Can have 0 or more arguments.
    """
    rule = (
        Optional(Template.rule("template"))  #
        + IDENT("name")  #
        + LPAREN  #
        + ArgumentList.rule("args_list")  #
        + RPAREN  #
        + SEMI_COLON  # BR
    ).setParseAction(lambda t: Constructor(t.name, t.args_list, t.template))

    def __init__(self,
                 name: str,
                 args: ArgumentList,
                 template: Union[Template, Any],
                 parent: Union["Class", Any] = ''):
        self.name = name
        self.args = args
        self.template = template

        self.parent = parent

    def __repr__(self) -> str:
        return "Constructor: {}{}".format(self.name, self.args)


class Operator:
    """
    Rule for parsing operator overloads.

    E.g.
    ```
    class Overload {
        Vector2 operator+(const Vector2 &v) const;
    };
    """
    rule = (
        ReturnType.rule("return_type")  #
        + Literal("operator")("name")  #
        + OPERATOR("operator")  #
        + LPAREN  #
        + ArgumentList.rule("args_list")  #
        + RPAREN  #
        + CONST("is_const")  #
        + SEMI_COLON  # BR
    ).setParseAction(lambda t: Operator(t.name, t.operator, t.return_type, t.
                                        args_list, t.is_const))

    def __init__(self,
                 name: str,
                 operator: str,
                 return_type: ReturnType,
                 args: ArgumentList,
                 is_const: str,
                 parent: Union["Class", Any] = ''):
        self.name = name
        self.operator = operator
        self.return_type = return_type
        self.args = args
        self.is_const = is_const
        self.is_unary = len(args) == 0

        self.parent = parent

        # Check for valid unary operators
        if self.is_unary and self.operator not in ('+', '-'):
            raise ValueError("Invalid unary operator {} used for {}".format(
                self.operator, self))

        # Check that number of arguments are either 0 or 1
        assert 0 <= len(args) < 2, \
            "Operator overload should be at most 1 argument, " \
                "{} arguments provided".format(len(args))

        # Check to ensure arg and return type are the same.
        if len(args) == 1 and self.operator not in ("()", "[]"):
            assert args.list()[0].ctype.typename.name == return_type.type1.typename.name, \
                "Mixed type overloading not supported. Both arg and return type must be the same."

    def __repr__(self) -> str:
        return "Operator: {}{}{}({}) {}".format(
            self.return_type,
            self.name,
            self.operator,
            self.args,
            self.is_const,
        )


class DunderMethod:
    """Special Python double-underscore (dunder) methods, e.g. __iter__, __contains__"""
    rule = (
        DUNDER  #
        + (Word(alphas))("name")  #
        + DUNDER  #
        + LPAREN  #
        + ArgumentList.rule("args_list")  #
        + RPAREN  #
        + SEMI_COLON  # BR
    ).setParseAction(lambda t: DunderMethod(t.name, t.args_list))

    def __init__(self, name: str, args: ArgumentList):
        self.name = name
        self.args = args

    def __repr__(self) -> str:
        return f"DunderMethod: __{self.name}__({self.args})"


class Class:
    """
    Rule to parse a class defined in the interface file.

    E.g.
    ```
    class Hello {
        ...
    };
    ```
    """

    class Members:
        """
        Rule for all the members within a class.
        """
        rule = ZeroOrMore(DunderMethod.rule  #
                          ^ Constructor.rule  #
                          ^ Method.rule  #
                          ^ StaticMethod.rule  #
                          ^ Variable.rule  #
                          ^ Operator.rule  #
                          ^ Enum.rule  #
                          ).setParseAction(lambda t: Class.Members(t.asList()))

        def __init__(self, members: List[Union[Constructor, Method,
                                               StaticMethod, Variable,
                                               Operator, Enum, DunderMethod]]):
            self.ctors = []
            self.methods = []
            self.dunder_methods = []
            self.static_methods = []
            self.properties = []
            self.operators = []
            self.enums: List[Enum] = []
            for m in members:
                if isinstance(m, Constructor):
                    self.ctors.append(m)
                elif isinstance(m, Method):
                    self.methods.append(m)
                elif isinstance(m, StaticMethod):
                    self.static_methods.append(m)
                elif isinstance(m, DunderMethod):
                    self.dunder_methods.append(m)
                elif isinstance(m, Variable):
                    self.properties.append(m)
                elif isinstance(m, Operator):
                    self.operators.append(m)
                elif isinstance(m, Enum):
                    self.enums.append(m)

    _parent = COLON + (TemplatedType.rule ^ Typename.rule)("parent_class")
    rule = (
        Optional(Template.rule("template"))  #
        + Optional(VIRTUAL("is_virtual"))  #
        + CLASS  #
        + IDENT("name")  #
        + Optional(_parent)  #
        + LBRACE  #
        + Members.rule("members")  #
        + RBRACE  #
        + SEMI_COLON  # BR
    ).setParseAction(lambda t: Class(
        t.template, t.is_virtual, t.name, t.parent_class, t.members.ctors, t.
        members.methods, t.members.static_methods, t.members.dunder_methods, t.
        members.properties, t.members.operators, t.members.enums))

    def __init__(
        self,
        template: Union[Template, None],
        is_virtual: str,
        name: str,
        parent_class: list,
        ctors: List[Constructor],
        methods: List[Method],
        static_methods: List[StaticMethod],
        dunder_methods: List[DunderMethod],
        properties: List[Variable],
        operators: List[Operator],
        enums: List[Enum],
        parent: Any = '',
    ):
        self.template = template
        self.is_virtual = is_virtual
        self.name = name
        if parent_class:
            # If it is in an iterable, extract the parent class.
            if isinstance(parent_class, Iterable):
                parent_class = parent_class[0]  # type: ignore

            # If the base class is a TemplatedType,
            # we want the instantiated Typename
            if isinstance(parent_class, TemplatedType):
                pass  # Note: this must get handled in InstantiatedClass

            self.parent_class = parent_class
        else:
            self.parent_class = ''  # type: ignore

        self.ctors = ctors
        self.methods = methods
        self.static_methods = static_methods
        self.dunder_methods = dunder_methods
        self.properties = properties
        self.operators = operators
        self.enums = enums

        self.parent = parent

        # Make sure ctors' names and class name are the same.
        for ctor in self.ctors:
            if ctor.name != self.name:
                raise ValueError("Error in constructor name! {} != {}".format(
                    ctor.name, self.name))

        for ctor in self.ctors:
            ctor.parent = self
        for method in self.methods:
            method.parent = self
        for static_method in self.static_methods:
            static_method.parent = self
        for dunder_method in self.dunder_methods:
            dunder_method.parent = self
        for _property in self.properties:
            _property.parent = self

    def namespaces(self) -> list:
        """Get the namespaces which this class is nested under as a list."""
        return collect_namespaces(self)

    def __repr__(self):
        return "Class: {self.name}".format(self=self)
